/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package ecodex.sellado;

import ecodex.serviciospruebas.Utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPathExpressionException;
import org.apache.commons.ssl.PKCS8Key;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;


/**
 *
 * @author Ecodex6
 */
public class EcodexSellado {
    Utils Utilidades = new Utils();
    private static final String CER_FILENAME = Utils.getPropertyValue("CER_FILE_NAME");
    private static final String KEY_FILENAME = Utils.getPropertyValue("KEY_FILE_NAME");
    private static final String PWD = Utils.getPropertyValue("PWD");

    /**
     * Sella el documento XML
     * @param filePath
     * @param generarDinamico
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws UnsupportedEncodingException
     * @throws XPathExpressionException 
     */
    public static String sellarXML(String filePath, boolean generarDinamico) 
            throws ParserConfigurationException, SAXException, UnsupportedEncodingException, XPathExpressionException{
        
        String sello;
        try {
            String xmlString = Utils.readFileAsString(filePath);
            String tipoDocumento = Utils.getRootNodeName(xmlString);
            
            if (generarDinamico)
                xmlString = setAtributos(xmlString, tipoDocumento);

            String cadenaOriginal = generateCadenaOriginal(xmlString, tipoDocumento);
            PrivateKey privateKey = getLlavePrivada(KEY_FILENAME, PWD);
            sello = generateSelloDigital(privateKey, cadenaOriginal);
            
            return setSello(xmlString, sello, tipoDocumento);
            
        } catch (FileNotFoundException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            sello = ex.getMessage();
        } catch (IOException | TransformerException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            sello = ex.getMessage();
        } catch (CertificateEncodingException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            sello = ex.getMessage();
        } catch (GeneralSecurityException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            sello = ex.getMessage();
        }
        
        return sello;
    }
    
    /**
     * Genera la cadena original
     * @param xmlString
     * @param tipoDocumento
     * @return
     * @throws CertificateEncodingException
     * @throws TransformerConfigurationException
     * @throws TransformerException 
     */
    private static String generateCadenaOriginal(final String xmlString, String tipoDocumento) 
            throws CertificateEncodingException, TransformerConfigurationException, TransformerException {
                
        StreamSource source = new StreamSource(getArchivoXSLT(tipoDocumento));
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer(source);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        transformer.transform(new StreamSource(new StringReader(xmlString)), new StreamResult(outputStream));
        
        return outputStream.toString();
    }
    
    /**
     * 
     * @param keyFile
     * @param password
     * @return
     * @throws FileNotFoundException
     * @throws GeneralSecurityException
     * @throws IOException 
     */
    private static PrivateKey getLlavePrivada(final String keyFile, final String password) 
            throws FileNotFoundException, GeneralSecurityException, IOException{
        
        FileInputStream inputFile = new FileInputStream(keyFile);
        PKCS8Key pcks8Key = new PKCS8Key(inputFile, password.toCharArray());
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(pcks8Key.getDecryptedBytes());
        PrivateKey privateKey = null;
        
        if (pcks8Key.isDSA()){
            privateKey = KeyFactory.getInstance("DSA").generatePrivate(spec);
        }
        else if (pcks8Key.isRSA()){
            privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
        }

        return privateKey;
    }

    /**
     * 
     * @param privateKey
     * @param cadenaOriginal
     * @return
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException 
     */
    private static String generateSelloDigital(PrivateKey privateKey, final String cadenaOriginal) 
            throws InvalidKeyException, NoSuchAlgorithmException, SignatureException{
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(privateKey, new SecureRandom());
        signature.update(cadenaOriginal.getBytes());
        byte[] bytesSignature = signature.sign();
        return new String(new org.apache.commons.codec.binary.Base64().encode(bytesSignature));
    }

    /**
     * 
     * @return
     * @throws CertificateException
     * @throws IOException 
     */
    private static Certificado getCertificado() throws CertificateException, IOException    {

        try {
            Certificado certificado;
            certificado = new Certificado();
            X509Certificate certificate = getCertificadoX509(new File(CER_FILENAME));
            certificado.setCertificadoBase64(getCertificadoBase64(certificate));
            certificado.setNoCertificado(getNumeroCertificado(certificate));
            
            return certificado;
        } catch (CertificateException | IOException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }        
    }        
    
    /**
     * 
     * @param certificateFile
     * @return
     * @throws CertificateException
     * @throws IOException 
     */
    private static X509Certificate getCertificadoX509(final File certificateFile) 
            throws CertificateException, IOException{
        FileInputStream certificate = null;
        
        try {
            certificate = new FileInputStream(certificateFile);
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            return (X509Certificate)factory.generateCertificate(certificate);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(EcodexSellado.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
        finally {
            if (certificate != null){
                certificate.close();
            }
        }
    }    
    
    /**
     * 
     * @param certificate
     * @return
     * @throws CertificateEncodingException 
     */
    private static String getCertificadoBase64(final X509Certificate certificate) 
            throws CertificateEncodingException {
        return new String(new org.apache.commons.codec.binary.Base64().encode(certificate.getEncoded()));
    }
    
    /**
     * 
     * @param certificate
     * @return 
     */
    private static String getNumeroCertificado(final X509Certificate certificate){
        BigInteger serial = certificate.getSerialNumber();
        byte[] bytesArray = serial.toByteArray();
        StringBuilder builder = new StringBuilder();
        
        for(int i = 0; i < bytesArray.length; i++){
            builder.append((char)bytesArray[i]);
        }
        
        return builder.toString();
    }
    
    /**
     * Obtiene el XSLT a utilizar para generar la cadena original dependiendo del tipo de documento
     * @param tipoDocumento
     * @return 
     */
    private static String getArchivoXSLT(String tipoDocumento) {
        if (null != tipoDocumento) switch (tipoDocumento) {
            case "Comprobante":
                return Utils.getPropertyValue("XSLT_CADENA_ORIGINAL_COMPROBANTE");
            case "Retenciones":
                return Utils.getPropertyValue("XSLT_CADENA_ORIGINAL_RETENCION");
        }
        
        return "Comprobante";
    }    
 
    /**
     * Establece el valor del sello en la cadena XML que contiene al comprobante
     * @param xmlString
     * @param sello
     * @param tipoDocumento
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException 
     */
    private static String setSello(String xmlString, String sello, String tipoDocumento) 
            throws ParserConfigurationException, SAXException, IOException, TransformerException {

        String nombreAtributo = tipoDocumento.equals("Comprobante") ? "sello" : "Sello";
        return setAtributo(xmlString, nombreAtributo, sello);
    }
    
    /**
     * Establece algunos atributos para generar un comprobante de manera dinámica
     * @param xmlString
     * @param tipoDocumento
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException
     * @throws CertificateException 
     */
    private static String setAtributos(String xmlString, String tipoDocumento) 
        throws ParserConfigurationException, SAXException, IOException, TransformerException, CertificateException {
        
            String fechaHoyString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()).replace(' ', 'T');
            Certificado certificado = getCertificado();
        
            if (tipoDocumento.equals("Comprobante")) {
                xmlString = setAtributo(xmlString, "fecha", fechaHoyString);
                xmlString = setAtributo(xmlString, "certificado", certificado.getCertificadoBase64());
                xmlString = setAtributo(xmlString, "noCertificado", certificado.getNoCertificado());                
            }
            else {
                xmlString = setAtributo(xmlString, "FechaExp", fechaHoyString + "-06:00");
                xmlString = setAtributo(xmlString, "Cert", certificado.getCertificadoBase64());
                xmlString = setAtributo(xmlString, "NumCert", certificado.getNoCertificado());                
            }            

            return xmlString;        
    }

    /**
     * Establece el valor de algun atributo del XML que es pasado como cadena (solo para el nodo raíz)
     * @param xmlString
     * @param nombreAtributo
     * @param valor
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException 
     */
    private static String setAtributo(String xmlString, String nombreAtributo, String valor) 
            throws ParserConfigurationException, SAXException, IOException, TransformerException {
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
        Document doc = docBuilder.parse(new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8)));
        Node nodo = doc.getFirstChild();
        NamedNodeMap atributos = nodo.getAttributes();
        Attr atributo = doc.createAttribute(nombreAtributo);
        atributo.setValue(valor);
        atributos.setNamedItem(atributo);
                
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StreamResult result = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(doc);
        transformer.transform(source, result);
        xmlString = result.getWriter().toString();

        return xmlString;            
    }
}
